package com.nononsenseapps.feeder.parser

import com.nononsenseapps.feeder.net.NetManager.Companion.defaultClient
import com.nononsenseapps.feeder.util.asFeed
import com.nononsenseapps.feeder.util.relativeLinkIntoAbsolute
import com.nononsenseapps.feeder.util.relativeLinkIntoAbsoluteOrThrow
import com.nononsenseapps.feeder.util.sloppyLinkToStrictURL
import com.nononsenseapps.jsonfeed.Feed
import com.nononsenseapps.jsonfeed.JsonFeedParser
import com.rometools.rome.io.ParsingFeedException
import com.rometools.rome.io.SyndFeedInput
import com.rometools.rome.io.XmlReader
import okhttp3.*
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import timber.log.Timber
import java.io.File
import java.io.InputStream
import java.net.MalformedURLException
import java.net.URL

object FeedParser : AbstractObjectParser<Feed>(JsonFeedParser(defaultClient)) {

     const val YOUTUBE_CHANNEL_ID_ATTR = "data-channel-external-id"

    /**
     * To enable caching, you need to call this method explicitly with a suitable cache directory.
     */
    @Synchronized
    override fun setup(cacheDir: File?): FeedParser {
        netManager.setup(cacheDir)
        jsonParser = JsonFeedParser(netManager.client)
        return this
    }

    /**
     * Finds the preferred alternate link in the header of an HTML/XML document pointing to feeds.
     */
    fun findFeedUrl(html: String,
                    preferRss: Boolean = false,
                    preferAtom: Boolean = false,
                    preferJSON: Boolean = false): URL? {

        val feedLinks = getAlternateFeedLinksInHtml(html)
                .sortedBy {
                    val t = it.second.lowercase()
                    when {
                        preferAtom && t.contains("atom") -> "0"
                        preferRss && t.contains("rss") -> "1"
                        preferJSON && t.contains("json") -> "2"
                        else -> t
                    }
                }
                .map {
                    sloppyLinkToStrictURL(it.first) to it.second
                }

        return feedLinks.firstOrNull()?.first
    }

    /**
     * Returns all alternate links in the header of an HTML/XML document pointing to feeds.
     */
    suspend fun getAlternateFeedLinksAtUrl(url: URL): List<Pair<String, String>> {
        return try {
            val html = netManager.curl(url)
            when {
                html != null -> getAlternateFeedLinksInHtml(html, baseUrl = url)
                else -> emptyList()
            }
        } catch (t: Throwable) {
            Timber.e("Error when fetching alternate links:$t")
            emptyList()
        }
    }

    /**
     * Returns all alternate links in the HTML/XML document pointing to feeds.
     */
    fun getAlternateFeedLinksInHtml(html: String, baseUrl: URL? = null): List<Pair<String, String>> {
        val doc = Jsoup.parse(html.byteInputStream(), "UTF-8", "")

        val feeds = doc.getElementsByAttributeValue("rel", "alternate")
            ?.asSequence()
            ?.filter { it.hasAttr("href") && it.hasAttr("type") }
            ?.filter {
                val t = it.attr("type").lowercase()
                when {
                    t.contains("application/atom") -> true
                    t.contains("application/rss") -> true
                    // Youtube for example has alternate links with application/json+oembed type.
                    t == "application/json" -> true
                    else -> false
                }
            }
            ?.filter {
                val l = it.attr("href").lowercase()
                try {
                    if (baseUrl != null) {
                        relativeLinkIntoAbsoluteOrThrow(base = baseUrl, link = l)
                    } else {
                        URL(l)
                    }
                    true
                } catch (_: MalformedURLException) {
                    false
                }
            }
            ?.map {
                when {
                    baseUrl != null -> relativeLinkIntoAbsolute(base = baseUrl, link = it.attr("href")) to it.attr("type")
                    else -> sloppyLinkToStrictURL(it.attr("href")).toString() to it.attr("type")
                }
            }
            ?.toList() ?: emptyList()

        return when {
            feeds.isNotEmpty() -> feeds
            baseUrl?.host == "www.youtube.com" || baseUrl?.host == "youtube.com" -> findFeedLinksForYoutube(doc)
            else -> emptyList()
        }
    }

    private fun findFeedLinksForYoutube(doc: Document): List<Pair<String, String>> {
        val channelId: String? = doc.body()?.getElementsByAttribute(YOUTUBE_CHANNEL_ID_ATTR)
                ?.firstOrNull()
                ?.attr(YOUTUBE_CHANNEL_ID_ATTR)

        return when (channelId) {
            null -> emptyList()
            else -> listOf("https://www.youtube.com/feeds/videos.xml?channel_id=$channelId" to "atom")
        }
    }

    /**
     * Takes body as bytes to handle encoding correctly
     */
    @Throws(ObjectParsingError::class)
     suspend fun parseFeedResponseOrFallbackToAlternateLink(response: Response): Feed? =
            response.body?.use { responseBody ->
                responseBody.bytes().let { body ->
                    // Encoding is not an issue for reading HTML (probably)
                    val alternateFeedLink = findFeedUrl(String(body), preferAtom = true)

                    return if (alternateFeedLink != null) {
                        parseObjectUrl(alternateFeedLink)
                    } else {
                        parseObjectResponse(response, body)
                    }
                }
            }


    /**
     * Takes body as bytes to handle encoding correctly
     */
    @Throws(ObjectParsingError::class)
    override fun parseObjectResponse(response: Response, body: ByteArray): Feed? {
        try {
            val isJSON = (response.header("content-type") ?: "").contains("json")

            val feed = when (isJSON) {
                true -> jsonParser.parseJsonBytes(body)
                false -> parseRssAtomBytes(response.request.url.toUrl(), body)
            }

            return if (feed.feed_url == null) {
                // Nice to return non-null value here
                feed.copy(feed_url = response.request.url.toString())
            } else {
                feed
            }
        } catch (e: Throwable) {
            throw ObjectParsingError(e,response.request.url.toString())
        }
    }

    @Throws(ObjectParsingError::class,ParsingFeedException::class)
    override  fun parseFeedInputStream(baseUrl: URL, `is`: InputStream): Feed{
        `is`.use {
            try {
                val feed = XmlReader(`is`).use { SyndFeedInput().build(it) }
                return feed.asFeed(baseUrl = baseUrl)
            } catch (e: NumberFormatException) {
                throw e
            }catch (e: ParsingFeedException) {
                throw e
            }
            catch (e: Throwable) {
                throw ObjectParsingError(e,baseUrl.toString())
            }
        }
    }
}
